package com.openway.game.field;

import com.openway.game.exception.FieldCoordinateOutOfBoundsException;
import com.openway.game.exception.FieldStateException;
import com.openway.game.exception.GameRuntimeException;
import com.openway.game.personage.GameObject;
import com.openway.game.personage.impl.Lamp;

import java.util.*;

/**
 * Implementation of {@link PlayingField} interface.
 *
 * @author Created: 01.03.12 12:52
 * @version $Date$ $Revision$
 */
public class PlayingFieldImpl implements PlayingField {
    private static final int PLAYING_FIELD_WIDTH = 15;
    private static final int PLAYING_FIELD_HEIGHT = 15;

    private GameObject[][] field;

    //These fields are created only for performance.
    //Allows to quickly get the object's coordinate (without loop through the array).
    private Map<GameObject, Coordinate> objectsCoordinates = new HashMap<GameObject, Coordinate>();
    private Lamp lamp;

    public PlayingFieldImpl() {
        field = new GameObject[PLAYING_FIELD_HEIGHT][PLAYING_FIELD_WIDTH];
    }

    @Override
    public void addObject(GameObject gameObject, Coordinate coordinate) {
        throwIfOutOfBounds(coordinate);

        GameObject currentObjectInCoordinate = field[coordinate.getRow()][coordinate.getColumn()];

        field[coordinate.getRow()][coordinate.getColumn()] = gameObject;
        objectsCoordinates.put(gameObject, coordinate);
        if (currentObjectInCoordinate != null)
            objectsCoordinates.remove(currentObjectInCoordinate);

        if (gameObject instanceof Lamp) {
            if (lamp != null)
                throw new FieldStateException("In current version only one lamp can be on the field");
            lamp = (Lamp) gameObject;
        }
    }

    @Override
    public boolean moveObject(GameObject object, Direction direction) {
        Coordinate objectCoordinate = objectsCoordinates.get(object);
        Coordinate targetCoordinate = getCoordinateInDirection(objectCoordinate, direction);

        field[objectCoordinate.getRow()][objectCoordinate.getColumn()] = null;

        if (isOnField(targetCoordinate)) {
            GameObject objectInNextCoordinate = field[targetCoordinate.getRow()][targetCoordinate.getColumn()];
            field[targetCoordinate.getRow()][targetCoordinate.getColumn()] = object;
            objectsCoordinates.put(object, targetCoordinate);
            if (objectInNextCoordinate != null)
                objectsCoordinates.remove(objectInNextCoordinate);

            return true;

        } else {  //leave the field
            objectsCoordinates.remove(object);
            return false;
        }
    }

    @Override
    public void removeObject(GameObject gameObject) {
        Coordinate coordinate = objectsCoordinates.get(gameObject);
        if (coordinate != null) {
            field[coordinate.getRow()][coordinate.getColumn()] = null;
            objectsCoordinates.remove(gameObject);
        }
    }

    @Override
    public GameObject getObject(Coordinate coordinate) {
        throwIfOutOfBounds(coordinate);
        return field[coordinate.getRow()][coordinate.getColumn()];
    }

    @Override
    public GameObject getAdjacentObject(Coordinate coordinate, Direction direction) {
        return getObject(getCoordinateInDirection(coordinate, direction));
    }

    @Override
    public Coordinate getObjectCoordinate(GameObject gameObject) {
        return objectsCoordinates.get(gameObject);
    }

    @Override
    public boolean isInsideDirection(GameObject gameObject, Direction direction) {
        Coordinate objectCoordinate = objectsCoordinates.get(gameObject);
        if (objectCoordinate == null)
            return false;

        return isOnField(getCoordinateInDirection(objectCoordinate, direction));
    }

    @Override
    public boolean isLighted(GameObject gameObject) {
        Coordinate coordinate = objectsCoordinates.get(gameObject);
        if (coordinate == null)
            return false;

        Coordinate lampCoordinate = objectsCoordinates.get(lamp);
        if (lampCoordinate == null)
            return false;
        return Math.abs(coordinate.getColumn() - lampCoordinate.getColumn()) <= lamp.getIlluminationRadius() &&
                Math.abs(coordinate.getRow() - lampCoordinate.getRow()) <= lamp.getIlluminationRadius();
    }

    /**
     * This method currently is not safe - returns array of links to game objects (drunkards, policeman etc.)
     * Customers should not change the state of these objects.
     *
     * TODO clone returning objects
     */
    @Override
    public GameObject[][] getFieldSnapshot() {
        GameObject[][] fieldSnapshot = new GameObject[PLAYING_FIELD_HEIGHT][PLAYING_FIELD_WIDTH];
        for (int i = 0; i < PLAYING_FIELD_HEIGHT; i++) {
            fieldSnapshot[i] = Arrays.copyOf(field[i], PLAYING_FIELD_WIDTH);
        }
        return fieldSnapshot;
    }

    private Coordinate getCoordinateInDirection(Coordinate coordinate, Direction direction) {
        switch (direction) {
            case LEFT:
                return new Coordinate(coordinate.getRow(), coordinate.getColumn() - 1);
            case RIGHT:
                return new Coordinate(coordinate.getRow(), coordinate.getColumn() + 1);
            case UP:
                return new Coordinate(coordinate.getRow() - 1, coordinate.getColumn());
            case DOWN:
                return new Coordinate(coordinate.getRow() + 1, coordinate.getColumn());
            default:
                //should never happened
                throw new GameRuntimeException("Invalid direction " + direction);
        }
    }

    private void throwIfOutOfBounds(Coordinate coordinate) {
        if (!isOnField(coordinate))
            throw new FieldCoordinateOutOfBoundsException("Coordinate " + coordinate + " is out of field's bounds");
    }

    private boolean isOnField(Coordinate coordinate) {
        return coordinate.getRow() >= 0 && coordinate.getColumn() >= 0 &&
                coordinate.getRow() < PLAYING_FIELD_HEIGHT && coordinate.getColumn() < PLAYING_FIELD_WIDTH;
    }
}
